//
//  Agora+Extensions.swift
//  Hermes
//
//  Created by FH on 2021/1/14.
//  Copyright © 2021 Agora. All rights reserved.
//

import Foundation

// Hermes Common

extension String : TypeConvertible {
    static func convert(value: Any?) -> String {
        if let strValue = value as? String {
            return strValue
        } else {
            return "\(value!)"
        }
    }
    
    static func tryConvert(value: Any?) -> String? {
        if let strValue = value as? String {
            return strValue
        } else if let value = value {
            let (valueType, valueStr) = getAnyValueStr(from: value)
            if valueStr == "nil" && valueType.contains("Swift.Optional") {
                return nil
            } else {
                return valueStr
            }
        } else {
            return nil
        }
    }
    
    func fileHash() -> String? {
        if FileManager.default.fileExists(atPath: self) {
            let fileURL = URL(fileURLWithPath: self)
            if let fileContent = try? String(contentsOf: fileURL, encoding: .utf8),
               var data = fileContent.data(using: .utf8) {
                var digestData = Data(count: Int(CC_SHA256_DIGEST_LENGTH))

                digestData.withUnsafeMutableBytes {digestBytes in
                    data.withUnsafeBytes {messageBytes in
                        CC_SHA256(messageBytes, CC_LONG(data.count), digestBytes)
                    }
                }
                return digestData.map { String(format: "%02hhx", $0) }.joined()
            }
        }
        return nil
    }
}

public class OrderedDictionary<Key: Hashable, Value> : Codable, ExpressibleByDictionaryLiteral {
    fileprivate var items = [(Key, Value)]()
    fileprivate var iterPos = 0
    
    private enum CodingKeys: String, CodingKey {
        case items
    }
    
    public required init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        let encoder = try values.decode([String].self, forKey: .items)
        for item in encoder {
            let split = item.components(separatedBy: "<$>")
            let keyStr = split[0]
            let valueStr = split[1]
            let keySplit = keyStr.components(separatedBy: "<*>")
            let key = getAnyValue(by: keySplit[0], and: keySplit[1]) as! Key
            let valueSplit = valueStr.components(separatedBy: "<*>")
            let value = getAnyValue(by: valueSplit[0], and: valueSplit[1]) as! Value
            self.items.append((key, value))
        }
    }
    
    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        var result = [String]()
        for (key, value) in self.items {
            let (keyType, keyStr) = getAnyValueStr(from: key)
            let (valueType, valueStr) = getAnyValueStr(from: value)
            let keyPart = "\(keyType)<*>\(keyStr)"
            let valuePart = "\(valueType)<*>\(valueStr)"
            result.append("\(keyPart)<$>\(valuePart)")
        }
        try container.encode(result, forKey: .items)
    }
    
    public required init(dictionaryLiteral elements: (Key, Value)...) {
        for (key, value) in elements {
            self.addValue(value, forKey: key)
        }
    }
    
    public init(items: [(Key, Value)]) {
        for (key, value) in items {
            self.addValue(value, forKey: key)
        }
    }
    
    var count: Int {
        return self.items.count
    }
    
    var first: (Key, Value)? {
        return self.items.first
    }
    
    var last: (Key, Value)? {
        return self.items.last
    }
    
    subscript(keyPath: Int) -> (Key, Value) {
        return self.items[keyPath]
    }
    
    subscript(keyPath: Key) -> Value? {
        get {
            return self.getValue(byKey: keyPath)
        }
        set {
            if let value = newValue {
                self.addValue(value, forKey: keyPath)
            }
        }
    }
    
    private func findIndex(_ key: Key) -> Array<(Key, Value)>.Index? {
        return self.items.firstIndex(where: { (k, _) in k == key })
    }
        
    func removeValue(_ forKey: Key) {
        if let index = self.findIndex(forKey) {
            self.items.remove(at: index)
        }
    }
    
    func removeAll() {
        self.items.removeAll()
    }
    
    func contains(byKey: Key) -> Bool {
        return self.findIndex(byKey) != nil
    }
    
    func getValue(byKey: Key) -> Value? {
        if let index = self.findIndex(byKey) {
            return self.items[index].1
        }
        return nil
    }
    
    func addValue(_ value: Value, forKey: Key) {
        if let index = self.findIndex(forKey) {
            self.items.replaceSubrange(index...index, with: [(forKey, value)])
        } else {
            self.items.append((forKey, value))
        }
    }
    
    func insertValue(_ value: Value, forKey: Key, at: Int) {
        self.items.insert((forKey, value), at: at)
    }
    
    func mergeValues(by dict: [Key : Value]) {
        for (key, value) in dict {
            self.addValue(value, forKey: key)
        }
    }
    
    func mergeValues(by dict: OrderedDictionary<Key, Value>) {
        for (key, value) in dict {
            self.addValue(value, forKey: key)
        }
    }
    
    func toArray() -> [(Key, Value)] {
        return self.items
    }
}

extension OrderedDictionary : Sequence, Equatable, CustomDebugStringConvertible {
    public func makeIterator() -> AnyIterator<(Key, Value)> {
        return AnyIterator<(Key, Value)> {[weak self] in
            if let self = self {
                if self.iterPos < self.items.count {
                    let pos = self.iterPos
                    self.iterPos += 1
                    return self.items[pos]
                } else {
                    self.iterPos = 0
                }
            }
            return nil
       }
    }
    
    public static func == (lhs: OrderedDictionary, rhs: OrderedDictionary) -> Bool {
        if lhs.count == rhs.count, type(of: lhs) == type(of: rhs) {
            for i in 0..<lhs.count {
                let (lhsKey, lhsValue) = lhs.items[i]
                let (rhsKey, rhsValue) = rhs.items[i]
                if (lhsKey == rhsKey) {
                    let (lhsValueType, lhsValueStr) = getAnyValueStr(from: lhsValue)
                    let (rhsValueType, rhsValueStr) = getAnyValueStr(from: rhsValue)
                    if !(lhsValueType == rhsValueType && lhsValueStr == rhsValueStr) {
                        return false
                    }
                } else {
                    return false
                }
            }
            return true
        }
        return false
    }
    
    var allKeys : [Key] {
        var keys = [Key]()
        for (key, _) in self.items {
            keys.append(key)
        }
        return keys
    }
    
    var allValues : [Value] {
        var values = [Value]()
        for (_, value) in self.items {
            values.append(value)
        }
        return values
    }
    
    public var debugDescription: String {
        var result = [String]()
        for (key, value) in self.items {
            result.append("\(key): \(value)")
        }
        return "OrderedDictionary<\(Key.self), \(Value.self)>: \r" + result.joined(separator: "\r")
    }
}

// REMARK: add RawEnumConvertible protocol to SDK's Enums

#if SDK_VERSION_330

extension AgoraCaptureBrightnessLevelType : RawEnumConvertible { }
extension AgoraCloudProxyType : RawEnumConvertible { }
extension AgoraLogLevel : RawEnumConvertible { }
extension AgoraUploadErrorReason : RawEnumConvertible { }

#endif

#if SDK_VERSION_300

extension AgoraAreaCode : RawEnumConvertible { }
extension AgoraAudioEffectPreset : RawEnumConvertible { }
extension AgoraAudienceLatencyLevelType : RawEnumConvertible { }
extension AgoraRtmpStreamingEvent : RawEnumConvertible { }
extension AgoraSuperResolutionStateReason : RawEnumConvertible { }
extension AgoraVideoCaptureType : RawEnumConvertible { }
extension AgoraVideoCodecTypeForStream : RawEnumConvertible { }
extension AgoraVoiceBeautifierPreset : RawEnumConvertible { }

#endif

#if SDK_VERSION_All

extension AgoraCameraCaptureOutputPreference : RawEnumConvertible { }
extension AgoraConnectionStateType : RawEnumConvertible { }
extension AgoraAudioRemoteState : RawEnumConvertible { }
extension AgoraMediaType : RawEnumConvertible { }
extension AgoraAudioOutputRouting : RawEnumConvertible { }
extension AgoraVideoRotation : RawEnumConvertible { }
extension AgoraAudioEqualizationBandFrequency : RawEnumConvertible { }
extension AgoraRtmpStreamingErrorCode : RawEnumConvertible { }
extension AgoraChannelMediaRelayError : RawEnumConvertible { }
extension AgoraMetadataType : RawEnumConvertible { }
extension AgoraVideoContentHint : RawEnumConvertible { }
extension AgoraAudioMixingStateCode : RawEnumConvertible { }
extension AgoraVideoQualityAdaptIndication : RawEnumConvertible { }
extension AgoraVideoCodecType : RawEnumConvertible { }
extension AgoraAudioProfile : RawEnumConvertible { }
extension AgoraChannelMediaRelayEvent : RawEnumConvertible { }
extension AgoraEncryptionMode : RawEnumConvertible { }
extension AgoraLocalVideoStreamState : RawEnumConvertible { }
extension AgoraLastmileProbeResultState : RawEnumConvertible { }
extension AgoraUserOfflineReason : RawEnumConvertible { }
extension AgoraAudioLocalError : RawEnumConvertible { }
extension AgoraVideoProfile : RawEnumConvertible { }
extension AgoraAudioScenario : RawEnumConvertible { }
extension AgoraVideoMirrorMode : RawEnumConvertible { }
extension AgoraRtcDefaultCameraPosition : RawEnumConvertible { }
extension AgoraStreamSubscribeState : RawEnumConvertible { }
extension AgoraMediaDeviceType : RawEnumConvertible { }
extension AgoraVideoCodecProfileType : RawEnumConvertible { }
extension AgoraAudioCodecProfileType : RawEnumConvertible { }
extension AgoraAudioSessionOperationRestriction : RawEnumConvertible { }
extension AgoraErrorCode : RawEnumConvertible { }
extension AgoraLogFilter : RawEnumConvertible { }
extension AgoraWarningCode : RawEnumConvertible { }
extension AgoraChannelMediaRelayState : RawEnumConvertible { }
extension AgoraRtmpStreamingState : RawEnumConvertible { }
extension AgoraRtmpStreamLifeCycle : RawEnumConvertible { }
extension AgoraVideoOutputOrientationMode : RawEnumConvertible { }
extension AgoraDegradationPreference : RawEnumConvertible { }
extension AgoraVideoStreamType : RawEnumConvertible { }
extension AgoraAudioVoiceChanger : RawEnumConvertible { }
extension AgoraConnectionChangedReason : RawEnumConvertible { }
extension AgoraAudioRemoteStateReason : RawEnumConvertible { }
extension AgoraAudioSampleRateType : RawEnumConvertible { }
extension AgoraAudioRawFrameOperationMode : RawEnumConvertible { }
extension AgoraNetworkQuality : RawEnumConvertible { }
extension AgoraInjectStreamStatus : RawEnumConvertible { }
extension AgoraVideoRemoteState : RawEnumConvertible { }
extension AgoraLighteningContrastLevel : RawEnumConvertible { }
extension AgoraLocalVideoStreamError : RawEnumConvertible { }
extension AgoraAudioMixingErrorCode : RawEnumConvertible { }
extension AgoraVideoRemoteStateReason : RawEnumConvertible { }
extension AgoraAudioReverbType : RawEnumConvertible { }
extension AgoraStreamPublishState : RawEnumConvertible { }
extension AgoraNetworkType : RawEnumConvertible { }
extension AgoraAudioReverbPreset : RawEnumConvertible { }
extension AgoraAudioRecordingQuality : RawEnumConvertible { }
extension AgoraAudioLocalState : RawEnumConvertible { }
extension AgoraUserPriority : RawEnumConvertible { }
extension AgoraStreamFallbackOptions : RawEnumConvertible { }

#endif

extension AgoraChannelProfile : ComplexEnumConvertible {
    static var allCaseItems: [AgoraChannelProfile] {
        return [.communication, .liveBroadcasting, .game]
    }
}
extension AgoraClientRole : ComplexEnumConvertible {
    static var allCaseItems: [AgoraClientRole] {
        return [.broadcaster, .audience]
    }
}

extension AgoraVideoBufferType : ComplexEnumConvertible {
    static var allCaseItems: [AgoraVideoBufferType] {
        return [.pixelBuffer, .rawData]
    }
}

extension AgoraVideoPixelFormat : ComplexEnumConvertible {
    static var allCaseItems: [AgoraVideoPixelFormat] {
        return [.I420, .BGRA, .NV12]
    }
}

extension AgoraVideoRenderMode : ComplexEnumConvertible {
    static var allCaseItems: [AgoraVideoRenderMode] {
        return [.hidden, .fit, .adaptive, .fill]
    }
}

extension ScriptsInvokeMode : ComplexEnumConvertible {
    static var allCaseItems: [ScriptsInvokeMode] {
        return [.automatic, .semiAutomatic, .manual]
    }
}

// REMARK: handle enum convert

let allComplexEnumTypes: [String : Any.Type] = ["AppGridLayoutType": AppGridLayoutType.self, "AppAppIdType": AppAppIdType.self,
                                                "AppTokenType": AppTokenType.self, "AppTokenValidTime": AppTokenValidTime.self,
                                                "AppVideoBitrate": AppVideoBitrate.self, "AppVideoDimension": AppVideoDimension.self,
                                                "AppVideoFrameRate": AppVideoFrameRate.self, "ScriptsInvokeMode": ScriptsInvokeMode.self]

protocol ComplexCaseIterable {
    static var allCaseDescriptions: [String] { get }
    static func getCaseItem(_ index: ComplexEnumIndex) -> ComplexCaseIterable?
    static func getValue(by index: ComplexEnumIndex) -> Any?
    static func getValue(index: Int) -> Any?
    static func getIndex(_ anyValue: Any) -> Int
    
    func getCurrIndex() -> Int
    func equal(other anyValue: Any) -> Bool
    
    static var hasCustomItem: Bool { get }
    var isCustomItem: Bool { get }
    var customItemValue: Any? { get }
}

protocol ComplexEnumConvertible : RawEnumConvertible, ComplexCaseIterable {
    static var allCaseItems: [Self] { get }
    static func getCaseItem(by index: ComplexEnumIndex) -> Self?
    func equal(_ other: Self) -> Bool
}

extension ComplexEnumConvertible {
    static var hasCustomItem: Bool { return false }
    var isCustomItem: Bool {
        let desc = "\(self)"
        return desc.starts(with: "custom(")
    }
    var customItemValue: Any? { return nil }
    
    static var allCaseDescriptions: [String] {
        return self.allCaseItems.map {
            let desc = "\($0)"
            if desc.starts(with: "custom(") {
                return "Custom Input"
            } else {
                return desc
            }
        }
    }
    
    static func getValue(by index: ComplexEnumIndex) -> Any? {
        return Self.getValue(index: index.value)
    }
    
    static func getValue(index: Int) -> Any? {
        if index < self.allCaseItems.count {
            return self.allCaseItems[index].anyRawValue
        }
        return nil
    }
    
    static func getCaseItem(_ index: ComplexEnumIndex) -> ComplexCaseIterable? {
        return self.getCaseItem(by: index)
    }
    
    static func getCaseItem(by index: ComplexEnumIndex) -> Self? {
        if index.value >= 0 && index.value < self.allCaseItems.count {
            return self.allCaseItems[index.value]
        }
        return nil
    }
    
    static func getIndex(_ anyValue: Any) -> Int {
        if let item = self.createInstance(by: anyValue) {
            let allCases = self.allCaseItems
            for i in 0..<allCases.count {
                let caseItem = allCases[i]
                if caseItem.equal(item) {
                    return i
                }
            }
        }
        return 0
    }
    
    func getCurrIndex() -> Int {
        return Self.getIndex(self)
    }
    
    func equal(other anyValue: Any) -> Bool {
        if let rhs = Self.createInstance(by: anyValue) {
            return equalsAny(lhs: self.anyRawValue, rhs: rhs.anyRawValue)
        }
        return false
    }
    
    func equal(_ other: Self) -> Bool {
        if self.isCustomItem && other.isCustomItem {
            return true
        } else {
            return equalsAny(lhs: self.anyRawValue, rhs: other.anyRawValue)
        }
    }
}

enum AppGridLayoutType : Int, ComplexEnumConvertible, CaseIterable {
    case Fixed
    case Paging
    case Scrolling
    
    var anyRawValue: Any {
        return self.rawValue
    }
    
    static var allCaseItems: [AppGridLayoutType] {
        return AppGridLayoutType.allCases
    }
}

enum AppAppIdType : ComplexEnumConvertible {
    case staticCommunication
    case staticBroadcasting
    case dynamicCommunication
    case dynamicBroadcasting
    case preformanceTest
    case custom(appId: String)
    
    static var hasCustomItem: Bool { return true }
    var customItemValue: Any? {
        if case .custom(let appId) = self {
            return appId
        }
        return nil
    }
    
    var anyRawValue: Any {
        switch self {
        case .staticCommunication:
            return "0c0b4b61adf94de1befd7cdd78a50444"
        case .staticBroadcasting:
            return "aab8b8f5a8cd4469a63042fcfafe7063"
        case .dynamicCommunication:
            return "f248443b95df480581e06497d620a48e"
        case .dynamicBroadcasting:
            return "80e54398fed94ae8a010acf782f569b7"
        case .preformanceTest:
            return "6b1865d1da1f438d974ba72de4cb1ddd"
        case .custom(let appId):
            return appId
        }
    }
    
    static var allCaseItems: [AppAppIdType] {
        return [.staticCommunication, .staticBroadcasting,
                .dynamicCommunication, .dynamicBroadcasting,
                .preformanceTest, .custom(appId: "")]
    }
    
    static func createInstance(by anyValue: Any) -> AppAppIdType? {
        if let nativeValue = anyValue as? String {
            for item in self.allCaseItems {
                if item.rawValue == nativeValue {
                    return item
                }
            }
            return Self.custom(appId: nativeValue)
        } else if let enumValue = anyValue as? Self {
            return enumValue
        }
        return nil
    }
    
    static func selectIndex(by tokenType: AppTokenType, and channelProfile: AgoraChannelProfile) -> Int {
        let isStatic = tokenType == .static
        switch (isStatic, channelProfile) {
        case (true, .communication):
            return 0
        case (true, .liveBroadcasting):
            return 1
        case (false, .communication):
            return 2
        case (false, .liveBroadcasting):
            return 3
        default:
            return 1
        }
    }
    
    static func getEnumCase(by index: ComplexEnumIndex) -> AppAppIdType {
        let items = self.allCaseItems
        let pos = index.value
        if pos < items.count {
            return items[pos]
        }
        return .custom(appId: "")
    }
    
    func equal(other anyValue: Any) -> Bool {
        if let rhs = Self.createInstance(by: anyValue) {
            return self == rhs
        }
        return false
    }
}

extension AppAppIdType : CustomStringConvertible, Equatable, ExpressibleByStringLiteral {
    init(stringLiteral value: String) {
        self = Self.createInstance(by: value)!
    }
    
    var description: String {
        switch self {
        case .preformanceTest:
            return "preformanceTest"
        case .staticCommunication:
            return "staticCommunication"
        case .staticBroadcasting:
            return "staticBroadcasting"
        case .dynamicCommunication:
            return "dynamicCommunication"
        case .dynamicBroadcasting:
            return "dynamicBroadcasting"
        default:
            return "Custom Input"
        }
    }
    
    var rawValue: String {
        return self.anyRawValue as! String
    }
    
    static func == (lhs: AppAppIdType, rhs: AppAppIdType) -> Bool {
        let result = (lhs, rhs)
        switch result {
        case (.preformanceTest, .preformanceTest), (.staticCommunication, .staticCommunication),
             (.staticBroadcasting, .staticBroadcasting), (.dynamicCommunication, .dynamicCommunication),
             (.dynamicBroadcasting, .dynamicBroadcasting), (.custom, .custom):
            return true
        default:
            return false
        }
    }
}

enum AppTokenType: Int, ComplexEnumConvertible, CaseIterable {
    case `static`
    case dynamic5
    case token
    case token2
    
    func getFetchURL() -> String {
        switch self {
        case .dynamic5:
            return "http://recording.agorapremium.agora.io:9001/agora/media/genDynamicKey5"
        case .token:
            return "http://recording.agorapremium.agora.io:9001/agora/media/genAccessToken2"
        case .token2:
            return "http://recording.agorapremium.agora.io:9001/agora/media/genDynamicKey7"
        default:
            return ""
        }
    }
    
    static var allCaseItems: [AppTokenType] {
        return self.allCases
    }
}

enum AppTokenValidTime : Int, ComplexEnumConvertible, CaseIterable {
    case infinity = 3600000
    case lessHalfMinute = 29
    case oneMinute = 60
    case threeMinutes = 180
    case fiveMinutes = 300
    
    static var allCaseItems: [AppTokenValidTime] {
        return self.allCases
    }
}

extension AppTokenValidTime : CustomStringConvertible {
    var description: String {
        return "\(self.rawValue)"
    }
}

enum AppVideoBitrate : ComplexEnumConvertible {
    case standard
    case compatible
    case custom(bitrate: Int)
    
    static var hasCustomItem: Bool { return true }
    var customItemValue: Any? {
        if case .custom(let bitrate) = self {
            return bitrate
        }
        return nil
    }

    var anyRawValue: Any {
        switch self {
        case .standard:
            return AgoraVideoBitrateStandard
        case .compatible:
            return AgoraVideoBitrateCompatible
        case .custom(let bitrate):
            return bitrate
        }
    }
    
    static var allCaseItems: [AppVideoBitrate] {
        return [.standard, .compatible, .custom(bitrate: 1000)]
    }
    
    static func createInstance(by anyValue: Any) -> AppVideoBitrate? {
        if let enumValue = anyValue as? Self {
            return enumValue
        } else {
            var value: Int? = nil
            if let nativeValue = anyValue as? Int {
                value = nativeValue
            } else if let strValue = anyValue as? String, let nativeValue = Int(strValue) {
                value = nativeValue
            }
            if let value = value {
                for item in self.allCaseItems {
                    if item.rawValue == value {
                        return item
                    }
                }
                return Self.custom(bitrate: value)
            }
        }
        return nil
    }
}

extension AppVideoBitrate : Equatable, ExpressibleByIntegerLiteral {
    init(integerLiteral value: Int) {
        self = Self.createInstance(by: value)!
    }
    
    var rawValue: Int {
        return self.anyRawValue as! Int
    }
    
    static func == (lhs: AppVideoBitrate, rhs: AppVideoBitrate) -> Bool {
        let result = (lhs, rhs)
        switch result {
        case (.standard, .standard), (.compatible, .compatible), (.custom, .custom):
            return true
        default:
            return false
        }
    }
}

enum AppVideoDimension : ComplexEnumConvertible {
    case dim120x120
    case dim160x120
    case dim180x180
    case dim240x180
    case dim320x180
    case dim240x240
    case dim320x240
    case dim424x240
    case dim360x360
    case dim480x360
    case dim640x360
    case dim480x480
    case dim640x480
    case dim840x480
    case dim960x720
    case dim1280x720
    case dim1920x1080
    case dim2540x1440
    case dim3840x2160
    case custom(size: CGSize)
    
    static var hasCustomItem: Bool { return true }
    var customItemValue: Any? {
        if case .custom(let size) = self {
            return size
        }
        return nil
    }
    
    var anyRawValue: Any {
        return self.toSize()
    }
    
    static var allCaseItems: [AppVideoDimension] {
        return [.dim120x120, .dim160x120, .dim180x180, .dim240x180, .dim320x180, .dim240x240, .dim320x240, .dim424x240,
                .dim360x360, .dim480x360, .dim640x360, .dim480x480, .dim640x480, .dim840x480, .dim960x720, .dim1280x720,
                .dim1920x1080, .dim2540x1440, .dim3840x2160, .custom(size: CGSize.zero)]
    }
    
    static func createInstance(by anyValue: Any) -> AppVideoDimension? {
        if let nativeValue = anyValue as? CGSize {
            for item in self.allCaseItems {
                if item.rawValue == nativeValue {
                    return item
                }
            }
            return Self.custom(size: nativeValue)
        } else if let enumValue = anyValue as? Self {
            return enumValue
        }
        return nil
    }
    
    private func toSize() -> CGSize {
        switch self {
        case .dim120x120:
            return AgoraVideoDimension120x120
        case .dim160x120:
            return AgoraVideoDimension160x120
        case .dim180x180:
            return AgoraVideoDimension180x180
        case .dim240x180:
            return AgoraVideoDimension240x180
        case .dim320x180:
            return AgoraVideoDimension320x180
        case .dim240x240:
            return AgoraVideoDimension240x240
        case .dim320x240:
            return AgoraVideoDimension320x240
        case .dim424x240:
            return AgoraVideoDimension424x240
        case .dim360x360:
            return AgoraVideoDimension360x360
        case .dim480x360:
            return AgoraVideoDimension480x360
        case .dim640x360:
            return AgoraVideoDimension640x360
        case .dim480x480:
            return AgoraVideoDimension480x480
        case .dim640x480:
            return AgoraVideoDimension640x480
        case .dim840x480:
            return AgoraVideoDimension840x480
        case .dim960x720:
            return AgoraVideoDimension960x720
        case .dim1280x720:
            return AgoraVideoDimension1280x720
        case .dim1920x1080:
            return AgoraVideoDimension1920x1080
        case .dim2540x1440:
            return AgoraVideoDimension2540x1440
        case .dim3840x2160:
            return AgoraVideoDimension3840x2160
        case .custom(let size):
            return size
        }
    }
}

extension AppVideoDimension : Equatable {
    init(_ size: CGSize) {
        self = Self.createInstance(by: size)!
    }
    
    var rawValue: CGSize {
        return self.anyRawValue as! CGSize
    }
    
    static func == (lhs: AppVideoDimension, rhs: AppVideoDimension) -> Bool {
        let result = (lhs, rhs)
        switch result {
        case (.custom, .custom):
            return true
        default:
            return lhs.rawValue == rhs.rawValue
        }
    }
}

enum AppVideoFrameRate : ComplexEnumConvertible {
    case fps1
    case fps7
    case fps10
    case fps15
    case fps24
    case fps30
    case fps60
    case custom(fps: Int)
    
    static var hasCustomItem: Bool { return true }
    var customItemValue: Any? {
        if case .custom(let fps) = self {
            return fps
        }
        return nil
    }
    
    var anyRawValue: Any {
        switch self {
        case .fps1:
            return 1
        case .fps7:
            return 7
        case .fps10:
            return 10
        case .fps15:
            return 15
        case .fps24:
            return 24
        case .fps30:
            return 30
        case .fps60:
            return 60
        case .custom(let fps):
            return fps
        }
    }
    
    static var allCaseItems: [AppVideoFrameRate] {
        return [.fps1, .fps7, .fps10, .fps15, .fps24, .fps30, .fps60, .custom(fps: 0)]
    }
    
    static func createInstance(by anyValue: Any) -> AppVideoFrameRate? {
        if let enumValue = anyValue as? Self {
            return enumValue
        } else {
            var value: Int? = nil
            if let nativeValue = anyValue as? Int {
                value = nativeValue
            } else if let strValue = anyValue as? String, let nativeValue = Int(strValue) {
                value = nativeValue
            }
            if let value = value {
                for item in self.allCaseItems {
                    if item.rawValue == value {
                        return item
                    }
                }
                return Self.custom(fps: value)
            }
        }
        return nil
    }
}

extension AppVideoFrameRate : Equatable, ExpressibleByIntegerLiteral {
    init(integerLiteral value: Int) {
        self = Self.createInstance(by: value)!
    }
    
    var rawValue: Int {
        return self.anyRawValue as! Int
    }
    
    static func == (lhs: AppVideoFrameRate, rhs: AppVideoFrameRate) -> Bool {
        let result = (lhs, rhs)
        switch result {
        case (.custom, .custom):
            return true
        default:
            return lhs.rawValue == rhs.rawValue
        }
    }
}

// REMARK: wrapper methods

struct ComplexEnumIndex {
    let value: Int
}

extension ComplexEnumIndex : ExpressibleByIntegerLiteral {
    init(integerLiteral value: Int) {
        self = ComplexEnumIndex(value: value)
    }
    
    static func + (lhs: ComplexEnumIndex, number: Int) -> ComplexEnumIndex {
        return (lhs.value + number).toComplexEnumIndex()
    }
    
    static func - (lhs: ComplexEnumIndex, number: Int) -> ComplexEnumIndex {
        return (lhs.value - number).toComplexEnumIndex()
    }
}

extension Int {
    func toComplexEnumIndex() -> ComplexEnumIndex {
        return ComplexEnumIndex(value: self)
    }
}

extension NSPopUpButton {
    var selectedItemTitle: String {
        return self.itemTitles[self.indexOfSelectedItem]
    }
    
    var selectedComplexEnumIndex: ComplexEnumIndex {
        return self.indexOfSelectedItem.toComplexEnumIndex()
    }
}

func getCaseItemDescriptions(by enumName: String, defaultItem: String?=nil, _ engine: ScriptEngine) -> [String] {
    var items = [String]()
    if let complexType = allComplexEnumTypes[enumName] as? ComplexCaseIterable.Type {
        items.append(contentsOf: complexType.allCaseDescriptions)
    } else if let node = engine.findAstNode(by: enumName), node.type == .enumerate {
        for caseItem in node.children {
            items.append(caseItem.name.replacingOccurrences(of: enumName, with: ""))
        }
    }
    if let defaultItem = defaultItem {
        items.insert(defaultItem, at: 0)
    }
    return items
}

func getEnumAnyValue(index: ComplexEnumIndex, _ enumName: String, _ engine: ScriptEngine) -> Any? {
    return getEnumAnyValue(by: index.value, enumName, engine)
}

func getEnumAnyValue(by selectedIndex: Int, _ enumName: String, _ engine: ScriptEngine) -> Any? {
    if let complexType = allComplexEnumTypes[enumName] as? ComplexCaseIterable.Type {
        return complexType.getValue(index: selectedIndex)
    } else if let value = engine.getEnumValue(enumName, byCaseIndex: selectedIndex) {
        if let intVal = value as? Int {
            return intVal
        } else if let uintVal = value as? UInt {
            return uintVal
        }
    }
    return nil
}

func selectIndexOfEnum(enumName: String="", _ selectValue: Any, _ engine: ScriptEngine) -> Int {
    if let complexType = allComplexEnumTypes[enumName] as? ComplexCaseIterable.Type {
        return complexType.getIndex(selectValue)
    } else if let complexEnum = selectValue as? ComplexCaseIterable {
        return complexEnum.getCurrIndex()
    } else {
        return engine.getEnumIndex(selectValue, enumType: enumName)
    }
}

func handleCustomComplexEnum(_ initValue: Any, _ select: NSPopUpButton,
                             _ selectItemFn: @escaping Action2<Bool, Any>, _ saveValueFn: @escaping Action1<Any>) {
    var complexType: ComplexCaseIterable.Type! = nil
    var caseIndex = 0
    var caseValue: Any? = nil
    var isCustomItem = false
    if let complex = initValue as? ComplexCaseIterable {
        caseIndex = complex.getCurrIndex()
        complexType = type(of: complex)
        if complex.isCustomItem {
            isCustomItem = true
            caseValue = complex.customItemValue
        }
    }
    if caseValue == nil, let raw = initValue as? RawEnumConvertible {
        caseValue = raw.anyRawValue
        select.selectItem(at: caseIndex)
    }
    if let val = caseValue {
        select.selectItem(at: caseIndex)
        selectItemFn(isCustomItem, val)
    }
    select.rac_eventTouch.subscribeNext { ctrl in
        let selectIndex = ctrl.selectedComplexEnumIndex
        if caseIndex != selectIndex.value, let selectCaseItem = complexType.getCaseItem(selectIndex) {
            caseIndex = selectIndex.value
            isCustomItem = selectCaseItem.isCustomItem
            if let caseValue = isCustomItem ? selectCaseItem.customItemValue : complexType.getValue(by: selectIndex) {
                selectItemFn(isCustomItem, caseValue)
                saveValueFn(caseValue)
            }
        }
    }
}
